<?php

    class CicpFileSystem implements Cicp_Handler_Interface
{
    const TWO_PI = 6.28318531;

    function initGraph($x, $y, $z)
    {
        $graph = new Cicp_Graph();
        $graph->setPosition($x, $y, $z);

        // create a menu for directories
        $directoryMenu = new Cicp_Graph_Menu('directoryMenu');
        $directoryMenu->addButton('Show contents');
        $directoryMenu->addButton('Hide contents');
        $directoryMenu->addButton('Recursive show contents');
        $directoryMenu->addButton('Route');

        // create a menu for files
        $fileMenu = new Cicp_Graph_Menu('fileMenu');
        $fileMenu->addButton('Route');
        
        // add menus to graph
        $graph->addMenu($fileMenu);
        $graph->addMenu($directoryMenu);

        // create the node for root directory
        $dirNode = $graph->createNode();
        $dirNode->setShape('sph');
        $dirNode->setDimensions(0, .2, 0);
        $dirNode->setColor(255, 0., 0.);
        $dirNode->setHlColor(255, 0, 0);
        $dirNode->setPosition(0, 0, 0);
        $dirNode->data = $_SERVER['DOCUMENT_ROOT'];
        $extendedMenu = clone $directoryMenu;
        $extendedMenu->addButton('Delete Graph', Cicp_Graph_Menu::BUTTON_DELETE);
        $extendedMenu->addButton('Reset Highlighting', Cicp_Graph_Menu::BUTTON_HL_RESET);
        $dirNode->assignMenu($extendedMenu);
        $graph->addMenu($extendedMenu);

        // show contents of root directory
        $this->showContents($graph, $dirNode);


        return $graph;
    }

    function menuChoice(Cicp_Graph $graph, Cicp_Graph_Node $node, Cicp_Graph_Menu $menu, $menuButtonLabel)
    {
        if ($menuButtonLabel == 'Show contents') {
            $this->showContents($graph, $node);
        }
        else if ($menuButtonLabel == 'Hide contents') {
            $this->hideContents($graph, $node->data);
        }
        else if ($menuButtonLabel == 'Recursive show contents') {
            $nodes = $graph->getNodes();
            foreach ($nodes as $gNode) {
                if ($this->isInDirectory($gNode->data, $node->data)) {
                    if (is_dir($gNode->data)) {
                        $this->menuChoice($graph, $gNode, $gNode->getMenu(), $menuButtonLabel);
                    }
                }
            }
        }
        else if ($menuButtonLabel == 'Route') {
            $this->highlightPath($graph, $node->data);
        }

        return $graph;
    }

    function isInDirectory($innerDir, $outerDir) {
        $strLen = strlen($innerDir);
        return (substr($outerDir, 0, $strLen) == $innerDir);
    }

    function showContents($graph, $dirNode)
    {
        $dir = realpath($dirNode->data);
        $handle = opendir($dir);
        $menus = $graph->getMenus();
        $directoryMenu = $menus['directoryMenu'];
        $fileMenu = $menus['fileMenu'];

        // configure directory node
        $dirNode->setTitle(basename($dir));
        $dirNodePosition = $dirNode->getNodeProperty('loc1');

        // read file system
        $files = array();
        $directories = array();
        while (false !== ($file = readdir($handle))) {
            if ($file == '.' || $file == '..') {
                continue;
            }
            else {
                $file = $dir . '/' . $file;
                if (is_dir($file)) {
                    $directories[] = $file;
                }
                else {
                    $files[] = $file;
                }
            }
        }

        $connectNode = new Cicp_Graph_Node;
        $connectNode->setShape('lin');
        $connectNode->setColor(255, 255, 255);

        // create file nodes
        $fileNode = new Cicp_Graph_Node;
        $fileNode->assignMenu($fileMenu);
        $fileNode->setShape('rec');
        $fileNode->setDimensions(.1, .2, 0.);
        $this->drawCircle($graph, $files, $connectNode, $dirNode,
                          $fileNode, 1,
                          new Cicp_Vector3d_Coordinate(0, 0, 2));

        // create dir nodes
        $directoryNode = new Cicp_Graph_Node;
        $directoryNode->assignMenu($directoryMenu);
        $directoryNode->setShape('sph');
        $directoryNode->setDimensions(0, .2, 0.);
        $this->drawCircle($graph, $directories, $connectNode, $dirNode,
                          $directoryNode, 2,
                          new Cicp_Vector3d_Coordinate(0, 0, 2));
    }

    function hideContents($graph, $directory)
    {
        // go through all graph nodes and remove the ones with
        // this path
        $nodes = $graph->getNodes();
        $removeNodes = array();
        $dirStr = $directory;
        $dirStrLen = strlen($dirStr);
        foreach ($nodes as $gNode) {
            if (substr($gNode->data, 0, $dirStrLen) == $dirStr) {
                if ($dirStrLen != strlen($gNode->data)) {
                    $removeNodes[] = $gNode;
                }
            }
        }
        // remove nodes
        foreach ($removeNodes as $n) {
            $graph->removeNode($n);
        }
    }

    function highlightPath($graph, $path)
    {
        // start at root node and highlight the route to the
        // specified node
        $nodes = $graph->getNodes();
        foreach ($nodes as $gNode) {
            if (is_dir($gNode->data)) {
                $leftStr = substr($path, 0, strlen($gNode->data));
                if ($leftStr == $gNode->data) {
                    $hlColor = $gNode->getNodeProperty('hl_color');
                    $color = $gNode->getNodeProperty('color');
                    // flip color/hlcolor
                    $gNode->setColor($hlColor->r,
                                     $hlColor->g,
                                     $hlColor->b);
                    $gNode->setHlColor($color->r,
                                       $color->g,
                                       $color->b);
                }
            }
        }
    }

    function drawCircle($graph, Array $entries, $lineNode, $centerNode, $entryNode, $radius = 1, $positionOffsets = null)
    {
        if ($positionOffsets == null) {
            $positionOffsets = new Cicp_Vector3d_Coordinate(0, 0, 0);
        }

        $angle = 0;
        $count = count($entries);
        $centerNodePosition = $centerNode->getNodeProperty('loc1');
        foreach ($entries as $entry) {
            $node = clone $entryNode;
            $node->data = $entry;
            $positionOffsets->x = $radius * sin($angle);
            $positionOffsets->y = $radius * cos($angle);
            $angle = $angle + self::TWO_PI / $count;

            $node->setPosition($centerNodePosition->x + $positionOffsets->x,
                               $centerNodePosition->z + $positionOffsets->z,
                               -1 * ($centerNodePosition->y + $positionOffsets->y)
                               );

            $node->setHlColor(255, 255, 0);
            $nodeColor = $this->getColorByFileType($node->data);
            // set color brightness according to file size
            $size = $this->getSize($node->data) / 1024;
            $size = $size - 512;
            if ($size <= 0) $size = 1;
            $sizeFactor = 255 / $size;
            $nodeColor = $nodeColor->multiply($sizeFactor);
            $node->setTitle(basename($entry) . "\n[" . $this->getReadableSize($this->getSize($entry)) . ']');

            $node->setColor($nodeColor->r,
                            $nodeColor->g,
                            $nodeColor->b);

            // set alpha of this node based on the file it represents
            $node->setAlpha($this->getFileAlpha($node->data));

            // connect to dirnode
            $connectNode = clone $lineNode;
            $connectNode->setPosition($centerNodePosition->x,
                                      $centerNodePosition->y,
                                      $centerNodePosition->z);
            $nodePosition = $node->getNodeProperty('loc1');
            $connectNode->setDimensions($nodePosition->x,
                                        $nodePosition->y,
                                        $nodePosition->z);
            $connectNode->data = $node->data;

            // set alpha of this node based on the file it represents
            $connectNode->setAlpha($this->getFileAlpha($node->data));

            // set colors
            $connectNode->setColor(255, 255, 255);
            $connectNode->setHlColor(255, 255, 0);

            $graph->registerNode($node);
            $graph->registerNode($connectNode);
        }
    }

    function getSize($file)
    {
        if (is_dir($file)) {
            return $this->getDirSize($file);
        }
        else {
            return filesize($file);
        }
    }

    function getFileAlpha($file)
    {
        if (is_dir($file)) {
            $base = dirname($file);
        }
        else {
            $base = basename($file);
        }

        if ($base{0} == '.') {
            return .5;
        }
        else {
            return 1.;
        }
    }

    function getColorByFileType($filename)
    {
        $resultVector = new Cicp_Vector3d_RgbColor(0, 0, 0);
        if (is_dir($filename)) {
            $resultVector->setValues(0, 0, 255);
        }
        else {
            $extension = substr($filename, strrpos($filename, '.')+1);
            switch ($extension) {
            case 'cmd':
            case 'exe':
            case 'com':
            case 'btm':
            case 'bat':
            case 'sh':
            case 'csh':
                $resultVector->setValues(0, 255, 0);
            break;
            case 'tar':
            case 'tgz':
            case 'arj':
            case 'taz':
            case 'lzh':
            case 'zip':
            case 'z':
            case 'Z':
            case 'gz':
            case 'bz2':
            case 'bz':
            case 'tbz2':
            case 'tz':
            case 'deb':
            case 'rpm':
            case 'jar':
            case 'rar':
            case 'ace':
            case 'zoo':
            case 'cpio':
            case '7z':
            case 'rz':
                $resultVector(255, 0, 0);
            break;

            case 'jpg':
            case 'jpeg':
            case 'gif':
            case 'bmp':
            case 'pbm':
            case 'pgm':
            case 'ppm':
            case 'tga':
            case 'xbm':
            case 'xpm':
            case 'tif':
            case 'tiff':
            case 'png':
            case 'mng':
            case 'pcx':
            case 'yuv':
            case 'mov':
            case 'mpg':
            case 'mpeg':
            case 'm2v':
            case 'mkv':
            case 'ogm':
            case 'mp4':
            case 'm4v':
            case 'mp4v':
            case 'vob':
            case 'qt':
            case 'nuv':
            case 'wmv':
            case 'asf':
            case 'rm':
            case 'rmvb':
            case 'flc':
            case 'avi':
            case 'fli':
            case 'gl':
            case 'dl':
            case 'xcf':
            case 'xwd':
                $resultVector->setValues(255, 0, 255);
            break;

            case 'pdf':
            case 'ps':
            case 'txt':
            case 'patch':
            case 'diff':
            case 'log':
            case 'tex':
            case 'doc':
                $resultVector->setValues(0, 255, 0);
            break;

            case 'flac':
            case 'mp3':
                $resultVector->setValues(255, 0, 255);
            break;

            case 'mpc':
            case 'ogg':
            case 'wav':
            case 'mid':
            case 'midi':
            case 'au':
            case 'flac':
            case 'aac':
            case 'ra':
            case 'mka':
                $resultVector->setValues(0, 255, 255);
            break;

            default:
                $resultVector->setValues(255, 255, 255);
            }
        }
        return $resultVector;
    }

    /**
     * Return human readable sizes
     *
     * @author      Aidan Lister <aidan@php.net>
     * @version     1.3.0
     * @link        http://aidanlister.com/repos/v/function.size_readable.php
     * @param       int     $size        size in bytes
     * @param       string  $max         maximum unit
     * @param       string  $system      'si' for SI, 'bi' for binary prefixes
     * @param       string  $retstring   return string format
     */
    function getReadableSize($size, $max = null, $system = 'si', $retstring = '%01.2f %s')
    {
        // Pick units
        $systems['si']['prefix'] = array('B', 'K', 'MB', 'GB', 'TB', 'PB');
        $systems['si']['size']   = 1000;
        $systems['bi']['prefix'] = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
        $systems['bi']['size']   = 1024;
        $sys = isset($systems[$system]) ? $systems[$system] : $systems['si'];

        // Max unit to display
        $depth = count($sys['prefix']) - 1;
        if ($max && false !== $d = array_search($max, $sys['prefix'])) {
            $depth = $d;
        }

        // Loop
        $i = 0;
        while ($size >= $sys['size'] && $i < $depth) {
            $size /= $sys['size'];
            $i++;
        }

        return sprintf($retstring, $size, $sys['prefix'][$i]);
    }

    /**
     * Calculate the size of a directory by iterating its contents
     *
     * @author      Aidan Lister <aidan@php.net>
     * @version     1.2.0
     * @link        http://aidanlister.com/repos/v/function.dirsize.php
     * @param       string   $directory    Path to directory
     */
    function getDirSize($path)
    {
        // Init
        $size = 0;

        // Trailing slash
        if (substr($path, -1, 1) !== DIRECTORY_SEPARATOR) {
            $path .= DIRECTORY_SEPARATOR;
        }

        // Sanity check
        if (is_file($path)) {
            return filesize($path);
        } elseif (!is_dir($path)) {
            return false;
        }

        // Iterate queue
        $queue = array($path);
        for ($i = 0, $j = count($queue); $i < $j; ++$i)
        {
            // Open directory
            $parent = $i;
            if (is_dir($queue[$i]) && $dir = @dir($queue[$i])) {
                $subdirs = array();
                while (false !== ($entry = $dir->read())) {
                    // Skip pointers
                    if ($entry == '.' || $entry == '..') {
                        continue;
                    }

                    // Get list of directories or filesizes
                    $path = $queue[$i] . $entry;
                    if (is_dir($path)) {
                        $path .= DIRECTORY_SEPARATOR;
                        $subdirs[] = $path;
                    } elseif (is_file($path)) {
                        $size += filesize($path);
                    }
                }

                // Add subdirectories to start of queue
                unset($queue[0]);
                $queue = array_merge($subdirs, $queue);

                // Recalculate stack size
                $i = -1;
                $j = count($queue);

                // Clean up
                $dir->close();
                unset($dir);
            }
        }

        return $size;
    }
}